/*
 * Copyright 2009 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.aptemo.webetek.client.widgets;

import java.util.Date;

import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.datepicker.client.DateBox;
import com.google.gwt.user.datepicker.client.DatePicker;

/**
 * A text box that shows a {@link DatePicker} when the user focuses on it.
 * 
 * <h3>CSS Style Rules</h3>
 * 
 * <dl>
 * <dt>.gwt-DateBox</dt>
 * <dd>default style name</dd>
 * <dt>.dateBoxPopup</dt>
 * <dd>Applied to the popup around the DatePicker</dd>
 * <dt>.dateBoxFormatError</dt>
 * <dd>Default style for when the date box has bad input. Applied by
 * {@link DateBox.DefaultFormat} when the text does not represent a date that
 * can be parsed</dd>
 * </dl>
 * 
 * <p>
 * <h3>Example</h3>
 * {@example com.google.gwt.examples.DateBoxExample}
 * </p>
 */
public class DateTimeBox
		extends Composite implements HasValue<Date>
{
	/**
	 * Default {@link DateBox.Format} class. The date is first parsed using the
	 * {@link DateTimeFormat} supplied by the user, or
	 * {@link DateTimeFormat#getMediumDateFormat()} by default.
	 * <p>
	 * If that fails, we then try to parse again using the default browser date
	 * parsing.
	 * </p>
	 * If that fails, the <code>dateBoxFormatError</code> css style is applied
	 * to the {@link DateBox}. The style will be removed when either a
	 * successful {@link #parse(DateBox,String, boolean)} is called or
	 * {@link #format(DateBox,Date)} is called.
	 * <p>
	 * Use a different {@link DateBox.Format} instance to change that behavior.
	 * </p>
	 */
	public static class DefaultFormat implements Format
	{

		private final DateTimeFormat	dateTimeFormat;

		/**
		 * Creates a new default format instance.
		 */
		public DefaultFormat()
		{
			dateTimeFormat = DateTimeFormat.getMediumDateTimeFormat();
		}

		/**
		 * Creates a new default format instance.
		 * 
		 * @param dateTimeFormat
		 *            the {@link DateTimeFormat} to use with this {@link Format}
		 *            .
		 */
		public DefaultFormat(DateTimeFormat dateTimeFormat)
		{
			this.dateTimeFormat = dateTimeFormat;
		}

		public String format(DateTimeBox box, Date date)
		{
			if (date == null)
			{
				return "";
			}
			else
			{
				return dateTimeFormat.format(date);
			}
		}

		/**
		 * Gets the date time format.
		 * 
		 * @return the date time format
		 */
		public DateTimeFormat getDateTimeFormat()
		{
			return dateTimeFormat;
		}

		@SuppressWarnings("deprecation")
		public Date parse(DateTimeBox dateBox, String dateText, boolean reportError)
		{
			Date date = null;
			try
			{
				if (dateText.length() > 0)
				{
					date = dateTimeFormat.parse(dateText);
				}
			}
			catch (IllegalArgumentException exception)
			{
				try
				{
					date = new Date(dateText);
				}
				catch (IllegalArgumentException e)
				{
					if (reportError)
					{
						dateBox.addStyleName(DATE_BOX_FORMAT_ERROR);
					}
					return null;
				}
			}
			return date;
		}

		public void reset(DateTimeBox dateBox, boolean abandon)
		{
			dateBox.removeStyleName(DATE_BOX_FORMAT_ERROR);
		}
	}

	/**
	 * Implemented by a delegate to handle the parsing and formating of date
	 * values. The default {@link Format} uses a new {@link DefaultFormat}
	 * instance.
	 */
	public interface Format
	{

		/**
		 * Formats the provided date. Note, a null date is a possible input.
		 * 
		 * @param dateBox
		 *            the date box you are formatting
		 * @param date
		 *            the date to format
		 * @return the formatted date as a string
		 */
		String format(DateTimeBox dateBox, Date date);

		/**
		 * Parses the provided string as a date.
		 * 
		 * @param dateBox
		 *            the date box
		 * @param text
		 *            the string representing a date
		 * @param reportError
		 *            should the formatter indicate a parse error to the user?
		 * @return the date created, or null if there was a parse error
		 */
		Date parse(DateTimeBox dateBox, String text, boolean reportError);

		/**
		 * If the format did any modifications to the date box's styling, reset
		 * them now.
		 * 
		 * @param abandon
		 *            true when the current format is being replaced by another
		 * @param dateBox
		 *            the date box
		 */
		void reset(DateTimeBox dateBox, boolean abandon);
	}

	private class DateBoxHandler implements ValueChangeHandler<Date>, FocusHandler, BlurHandler, ClickHandler, KeyDownHandler, CloseHandler<PopupPanel>
	{

		public void onBlur(BlurEvent event)
		{
			if (isDatePickerShowing() == false)
			{
				updateDateFromTextBox();
			}
		}

		public void onClick(ClickEvent event)
		{
			showDatePicker();
		}

		public void onClose(CloseEvent<PopupPanel> event)
		{
			// If we are not closing because we have picked a new value, make
			// sure the
			// current value is updated.
			if (allowDPShow)
			{
				updateDateFromTextBox();
			}
		}

		public void onFocus(FocusEvent event)
		{
			if (allowDPShow && isDatePickerShowing() == false)
			{
				showDatePicker();
			}
		}

		public void onKeyDown(KeyDownEvent event)
		{
			switch (event.getNativeKeyCode())
			{
				case KeyCodes.KEY_ENTER:
				case KeyCodes.KEY_TAB:
					updateDateFromTextBox();
					// Deliberate fall through
				case KeyCodes.KEY_ESCAPE:
				case KeyCodes.KEY_UP:
					hideDatePicker();
					break;
				case KeyCodes.KEY_DOWN:
					showDatePicker();
					break;
			}
		}

		public void onValueChange(ValueChangeEvent<Date> event)
		{
			setValue(parseDate(false), event.getValue(), true);
			hideDatePicker();
			preventDatePickerPopup();
			box.setFocus(true);
		}
	}

	private class TimeBoxHandler implements FocusHandler, BlurHandler, ClickHandler, KeyDownHandler, CloseHandler<PopupPanel>, ChangeHandler
	{

		public void onBlur(BlurEvent event)
		{
			if (isTimePickerShowing() == false)
			{
				updateTimeFromTextBox();
			}
		}

		public void onClick(ClickEvent event)
		{
			showTimePicker();
		}

		public void onClose(CloseEvent<PopupPanel> event)
		{
			// If we are not closing because we have picked a new value, make
			// sure the
			// current value is updated.
			if (allowTPShow)
			{
				updateTimeFromTextBox();
			}
		}

		public void onFocus(FocusEvent event)
		{
			if (allowTPShow && isTimePickerShowing() == false)
			{
				showTimePicker();
			}
		}

		public void onKeyDown(KeyDownEvent event)
		{
			switch (event.getNativeKeyCode())
			{
				case KeyCodes.KEY_ENTER:
				case KeyCodes.KEY_TAB:
					updateTimeFromTextBox();
					// Deliberate fall through
				case KeyCodes.KEY_ESCAPE:
				case KeyCodes.KEY_UP:
					hideTimePicker();
					break;
				case KeyCodes.KEY_DOWN:
					showTimePicker();
					break;
			}
		}

		@Override
		public void onChange(ChangeEvent event)
		{
			Date dt = getValue();
			if (event.getSource() == timepicker)
			{
				int ind = timepicker.getSelectedIndex();
				dt.setHours(ind + 1);
				// dt.setMinutes(minutes);
			}
			else if (event.getSource() == timebox)
			{
				String text = timebox.getText();
				Date tb = timeFormatter.getShortTimeFormat().parse(text);
				dt.setHours(tb.getHours());
				dt.setMinutes(tb.getMinutes());
				dt.setSeconds(0);
			}

			setValue(parseDate(false), dt, true);
			hideTimePicker();
			preventTimePickerPopup();
			timebox.setFocus(true);
		}
	}

	/**
	 * Default style name added when the date box has a format error.
	 */
	private static final String			DATE_BOX_FORMAT_ERROR	= "dateBoxFormatError";

	/**
	 * Default style name.
	 */
	public static final String			DEFAULT_STYLENAME		= "gwt-DateBox";
	private static final DefaultFormat	DEFAULT_FORMAT			= GWT.create(DefaultFormat.class);
	private final PopupPanel			popup;
	private final TextBox				box						= new TextBox();
	private final DatePicker			picker;
	private Format						format;
	private boolean						allowDPShow				= true;

	private final TextBox				timebox					= new TextBox();
	private ListBox						timepicker;

	private PopupPanel					timepopup;

	private DateTimeFormat				timeFormatter			= DateTimeFormat.getShortTimeFormat();

	private boolean						allowTPShow				= true;

	/**
	 * Create a date box with a new {@link DatePicker}.
	 */
	public DateTimeBox()
	{
		this(new DatePicker(), new ListBox(), null, DEFAULT_FORMAT);
	}

	/**
	 * Create a new date box.
	 * 
	 * @param date
	 *            the default date.
	 * @param picker
	 *            the picker to drop down from the date box
	 * @param format
	 *            to use to parse and format dates
	 */
	public DateTimeBox(DatePicker picker, ListBox timepicker, Date date, Format format)
	{
		this.picker = picker;
		this.timepicker = timepicker;
		timepicker.setVisibleItemCount(12);
		Date tmp = new Date();
		for (int i = 0; i < 24; i++)
		{
			tmp.setTime(i * 3600000);
			String ts = timeFormatter.format(tmp);
			timepicker.addItem(ts);
		}
		this.popup = new PopupPanel(true);
		assert format != null : "You may not construct a date box with a null format";
		this.format = format;

		popup.addAutoHidePartner(box.getElement());
		popup.setWidget(picker);
		popup.setStyleName("dateBoxPopup");

		this.timepopup = new PopupPanel(true);
		timepopup.addAutoHidePartner(timebox.getElement());
		timepopup.setWidget(timepicker);
		timepopup.setStyleName("dateBoxPopup");

		HorizontalPanel panel = new HorizontalPanel();
		panel.add(box);
		panel.add(timebox);
		initWidget(panel);
		setStyleName(DEFAULT_STYLENAME);

		DateBoxHandler handler = new DateBoxHandler();
		picker.addValueChangeHandler(handler);
		box.addFocusHandler(handler);
		box.addBlurHandler(handler);
		box.addClickHandler(handler);
		box.addKeyDownHandler(handler);
		popup.addCloseHandler(handler);

		TimeBoxHandler than = new TimeBoxHandler();
		timepicker.addChangeHandler(than);
		timebox.addChangeHandler(than);
		timebox.addClickHandler(than);
		timebox.addKeyDownHandler(than);
		// timebox.addFocusHandler(than);
		// timebox.addBlurHandler(than);

		setValue(date);
	}

	public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Date> handler)
	{
		return addHandler(handler, ValueChangeEvent.getType());
	}

	/**
	 * Gets the current cursor position in the date box.
	 * 
	 * @return the cursor position
	 * 
	 */
	public int getCursorPos()
	{
		return box.getCursorPos();
	}

	/**
	 * Gets the date picker.
	 * 
	 * @return the date picker
	 */
	public DatePicker getDatePicker()
	{
		return picker;
	}

	/**
	 * Gets the format instance used to control formatting and parsing of this
	 * {@link DateBox}.
	 * 
	 * @return the format
	 */
	public Format getFormat()
	{
		return this.format;
	}

	/**
	 * Gets the date box's position in the tab index.
	 * 
	 * @return the date box's tab index
	 */
	public int getTabIndex()
	{
		return box.getTabIndex();
	}

	/**
	 * Get text box.
	 * 
	 * @return the text box used to enter the formatted date
	 */
	public TextBox getTextBox()
	{
		return box;
	}

	/**
	 * Get the date displayed, or null if the text box is empty, or cannot be
	 * interpreted.
	 * 
	 * @return the current date value
	 */
	public Date getValue()
	{
		return parseDate(true);
	}

	/**
	 * Hide the date picker.
	 */
	public void hideDatePicker()
	{
		popup.hide();
	}

	/**
	 * Hide the date picker.
	 */
	public void hideTimePicker()
	{
		timepopup.hide();
	}

	/**
	 * @return true if date picker is currently showing, false if not
	 */
	public boolean isDatePickerShowing()
	{
		return popup.isShowing();
	}

	/**
	 * @return true if date picker is currently showing, false if not
	 */
	public boolean isTimePickerShowing()
	{
		return timepopup.isShowing();
	}

	/**
	 * Sets the date box's 'access key'. This key is used (in conjunction with a
	 * browser-specific modifier key) to automatically focus the widget.
	 * 
	 * @param key
	 *            the date box's access key
	 */
	public void setAccessKey(char key)
	{
		box.setAccessKey(key);
	}

	/**
	 * Sets whether the date box is enabled.
	 * 
	 * @param enabled
	 *            is the box enabled
	 */
	public void setEnabled(boolean enabled)
	{
		box.setEnabled(enabled);
		timebox.setEnabled(enabled);
	}

	/**
	 * Explicitly focus/unfocus this widget. Only one widget can have focus at a
	 * time, and the widget that does will receive all keyboard events.
	 * 
	 * @param focused
	 *            whether this widget should take focus or release it
	 */
	public void setFocus(boolean focused)
	{
		box.setFocus(focused);
	}

	/**
	 * Sets the format used to control formatting and parsing of dates in this
	 * {@link DateBox}. If this {@link DateBox} is not empty, the contents of
	 * date box will be replaced with current contents in the new format.
	 * 
	 * @param format
	 *            the new date format
	 */
	public void setFormat(Format format)
	{
		assert format != null : "A Date box may not have a null format";
		if (this.format != format)
		{
			Date date = getValue();

			// This call lets the formatter do whatever other clean up is
			// required to
			// switch formatters.
			//
			this.format.reset(this, true);

			// Now update the format and show the current date using the new
			// format.
			this.format = format;
			setValue(date);
		}
	}

	/**
	 * Sets the date box's position in the tab index. If more than one widget
	 * has the same tab index, each such widget will receive focus in an
	 * arbitrary order. Setting the tab index to <code>-1</code> will cause this
	 * widget to be removed from the tab order.
	 * 
	 * @param index
	 *            the date box's tab index
	 */
	public void setTabIndex(int index)
	{
		box.setTabIndex(index);
	}

	/**
	 * Set the date.
	 */
	public void setValue(Date date)
	{
		setValue(date, false);
	}

	public void setValue(Date date, boolean fireEvents)
	{
		setValue(picker.getValue(), date, fireEvents);
	}

	/**
	 * Parses the current date box's value and shows that date.
	 */
	public void showDatePicker()
	{
		Date current = parseDate(false);
		if (current == null)
		{
			current = new Date();
		}
		picker.setCurrentMonth(current);
		popup.showRelativeTo(this);
	}

	/**
	 * Parses the current date box's value and shows that date.
	 */
	public void showTimePicker()
	{
		Date current = parseDate(false);
		if (current == null)
		{
			current = new Date();
		}
		timepicker.setSelectedIndex(0);
		timepopup.showRelativeTo(timebox);
	}

	private Date parseDate(boolean reportError)
	{
		if (reportError)
		{
			getFormat().reset(this, false);
		}
		String text = box.getText().trim();
		return getFormat().parse(this, text, reportError);
	}

	private void preventDatePickerPopup()
	{
		allowDPShow = false;
		DeferredCommand.addCommand(new Command()
		{
			public void execute()
			{
				allowDPShow = true;
			}
		});
	}

	private void preventTimePickerPopup()
	{
		allowTPShow = false;
		DeferredCommand.addCommand(new Command()
		{
			public void execute()
			{
				allowTPShow = true;
			}
		});
	}

	private void setValue(Date oldDate, Date date, boolean fireEvents)
	{
		if (date != null)
		{
			picker.setCurrentMonth(date);
			timebox.setText(timeFormatter.format(date));
		}
		picker.setValue(date, false);
		format.reset(this, false);
		box.setText(getFormat().format(this, date));

		if (fireEvents)
		{
			// DateChangeEvent.fireIfNotEqualDates(this, oldDate, date);
		}
	}

	private void updateDateFromTextBox()
	{
		Date parsedDate = parseDate(true);
		if (parsedDate != null)
		{
			setValue(picker.getValue(), parsedDate, true);
		}
	}

	private void updateTimeFromTextBox()
	{
		Date parsedDate = parseDate(true);
		if (parsedDate != null)
		{
			// setValue(timepicker.getValue(0), parsedDate, true);
		}
	}
}
